2024_riley_dorian_reeds.py

#

SPDX-FileCopyrightText: 2025 Florian Dubois SPDX-FileCopyrightText: 2025 AlICe laboratory https://alicelab.be

SPDX-License-Identifier: GPL-3.0-or-later

#

Florian Dubois 23/01/2025 Blender 4.2.2

import bpy
import math
import random
import mathutils
#

1) Nettoyer la scène : supprimer tous les objets sauf la caméra

for obj in bpy.data.objects:
    if obj.type != "CAMERA":
        bpy.data.objects.remove(obj, do_unlink=True)

bpy.ops.outliner.orphans_purge()
#

2) Ajouter un cube de taille 100 (wireframe)

cube_size = 100
bpy.ops.mesh.primitive_cube_add(size=cube_size, location=(0, 0, 0))
cube = bpy.context.object
#

Appliquer un modificateur Wireframe

wireframe_mod = cube.modifiers.new(name="Wireframe", type="WIREFRAME")
wireframe_mod.use_replace = True
wireframe_mod.thickness = 1  # ajuster au besoin
#

3) Fonctions utilitaires (vecteur aléatoire, réflexion, intersection)

def random_unit_vector():
    theta = random.uniform(0, 2 * math.pi)  # Angle autour de l'axe Z
    phi = random.uniform(0, 2 * math.pi)  # Angle de l'axe Z vers l'axe Y
    x = math.sin(phi) * math.cos(theta)
    y = math.sin(phi) * math.sin(theta)
    z = math.cos(phi) * math.sin(theta)

    if durée == 5:
        y = 0

    elif durée % 5 == 0:
        x = 0
        y = 90 * math.pi / 180

    else:
        diff = durée
        while diff > 5:
            diff -= 5
        y = y * diff
        x = x * diff
        z = z * diff

    return mathutils.Vector((x, y, z)).normalized()
#

Retourne la réflexion d’un vecteur par rapport à une normale.

def reflect_vector(vector, normal):
#
    return vector - 2 * vector.dot(normal) * normal
#

Calcule où un rayon (origin + t*direction) intersecte en premier le cube de demi-taille half_size.

def find_intersection(origin, direction, half_size):
#
    epsilon = 1e-5
    scales = [
        (half_size - origin.x) / direction.x if direction.x != 0 else float("inf"),
        (-half_size - origin.x) / direction.x if direction.x != 0 else float("inf"),
        (half_size - origin.y) / direction.y if direction.y != 0 else float("inf"),
        (-half_size - origin.y) / direction.y if direction.y != 0 else float("inf"),
        (half_size - origin.z) / direction.z if direction.z != 0 else float("inf"),
        (-half_size - origin.z) / direction.z if direction.z != 0 else float("inf"),
    ]
    valid_scales = [s for s in scales if s > epsilon and s != float("inf")]
    if valid_scales:
        scale = min(valid_scales)
    else:
#

Aucun scale strictement positif On peut choisir de ne pas créer de cylindre, ou de mettre scale=0

        scale = 0.0

    point = origin + direction * scale
#

Clamp pour que le point reste bien dans la surface (à ± half_size)

    point.x = max(-half_size, min(half_size, point.x))
    point.y = max(-half_size, min(half_size, point.y))
    point.z = max(-half_size, min(half_size, point.z))
    return point
#

4) Paramétrage : éviter des diamètres trop fins

fraction_min = 0.4  # intensité=1 => 40% du diamètre max
fraction_max = 1.0  # intensité=8 => 100% du diamètre max
steps = 7  # (8 - 1)
#

Calcule la fraction du diamètre max en fonction de l’intensité (1..8). intensité = 1 => fraction_min intensité = 8 => fraction_max

def fraction_for_intensity(i):
#
    i_clamp = max(1, min(8, i))
    return fraction_min + (i_clamp - 1) * (fraction_max - fraction_min) / steps
#

5) Base de données

variables = [
    {"durée": 2, "répétition": True, "notes": 4, "type": "Sixteenth"},  # 0
    {"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"},  # 1
    {"durée": 5, "répétition": False, "notes": 2, "type": "Long_held"},  # 2
    {"durée": 2, "répétition": True, "notes": 3, "type": "Sixteenth"},  # 3
    {"durée": 2, "répétition": True, "notes": 1, "type": "Sixteenth"},  # 4
    {"durée": 5, "répétition": False, "notes": 1, "type": "Long_held"},  # 5
    {"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"},  # 6
    {"durée": 15, "répétition": False, "notes": 8, "type": "Long_staccato"},  # 7
    {"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"},  # 8
    {"durée": 2, "répétition": True, "notes": 5, "type": "Sixteenth"},  # 9
    {"durée": 16, "répétition": False, "notes": 10, "type": "Long_staccato"},  # 10
    {"durée": 4, "répétition": True, "notes": 15, "type": "Sixteenth"},  # 11
    {"durée": 15, "répétition": False, "notes": 12, "type": "Long_staccato"},  # 12
    {"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"},  # 13
    {"durée": 14, "répétition": False, "notes": 10, "type": "Long_staccato"},  # 14
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 15
    {"durée": 15, "répétition": False, "notes": 9, "type": "Long_staccato"},  # 16
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 17
    {"durée": 17, "répétition": False, "notes": 19, "type": "Long_staccato"},  # 18
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 19
    {"durée": 33, "répétition": False, "notes": 10, "type": "Long_held"},  # 20
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 21
    {"durée": 36, "répétition": False, "notes": 4, "type": "Long_held"},  # 22
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 23
    {"durée": 30, "répétition": False, "notes": 17, "type": "Long_staccato"},  # 24
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 25
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 26
    {"durée": 24, "répétition": False, "notes": 10, "type": "Long_held"},  # 27
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 28
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 29
    {"durée": 3, "répétition": True, "notes": 12, "type": "Sixteenth"},  # 30
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 31
    {"durée": 5, "répétition": False, "notes": 1, "type": "Long_staccato"},  # 32
    {"durée": 7, "répétition": False, "notes": 4, "type": "Long_staccato"},  # 33
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 34
    {"durée": 8, "répétition": False, "notes": 1, "type": "Long_held"},  # 35
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 36
    {"durée": 6, "répétition": True, "notes": 18, "type": "Sixteenth"},  # 37
    {"durée": 4, "répétition": True, "notes": 4, "type": "Sixteenth"},  # 38
    {"durée": 3, "répétition": True, "notes": 10, "type": "Sixteenth"},  # 39
    {"durée": 3, "répétition": False, "notes": 8, "type": "Sixteenth"},  # 40
    {"durée": 5, "répétition": True, "notes": 7, "type": "Sixteenth"},  # 41
    {"durée": 20, "répétition": False, "notes": 38, "type": "Sixteenth"},  # 42
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 43
    {"durée": 5, "répétition": False, "notes": 1, "type": "Long_staccato"},  # 44
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 45
    {"durée": 6, "répétition": True, "notes": 19, "type": "Sixteenth"},  # 46
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 47
    {"durée": 2, "répétition": True, "notes": 5, "type": "Sixteenth"},  # 48
    {"durée": 30, "répétition": False, "notes": 21, "type": "Long_staccato"},  # 49
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 50
    {"durée": 3, "répétition": True, "notes": 1, "type": "Sixteenth"},  # 51
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 52
    {"durée": 5, "répétition": False, "notes": 1, "type": "Long_staccato"},  # 53
    {"durée": 5, "répétition": True, "notes": 11, "type": "Sixteenth"},  # 54
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 55
    {"durée": 5, "répétition": False, "notes": 1, "type": "Long_held"},  # 56
    {"durée": 4, "répétition": True, "notes": 11, "type": "Sixteenth"},  # 57
    {"durée": 2, "répétition": True, "notes": 1, "type": "Sixteenth"},  # 58
    {"durée": 3, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 59
    {"durée": 3, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 60
    {"durée": 14, "répétition": False, "notes": 3, "type": "Long_held"},  # 61
]
#

6) Sélection du pattern et filtrage

Patern_choisi = 11  # indice voulu (depuis la partition)
Patern_ciblé = Patern_choisi - 1
Patern_ciblé_biblio = Patern_ciblé
longueur_interval = 3
#

Filtrer la bibliothèque : extraire 3 éléments autour du pattern

bibliothèque_filtrée = variables[Patern_ciblé_biblio - 1 : Patern_ciblé_biblio + 2]
#

Pattern cible

pattern_cible = variables[Patern_ciblé]
rep = 1 if pattern_cible["répétition"] else 0
#

7) Génération des objets (troncs de cône) + sphère à chaque jonction

patern_en_cours = 0
half_size = cube_size / 2

if rep == 0:
    for variable in bibliothèque_filtrée:

        patern_en_cours += 1

        notes = variable["notes"]
        durée = variable["durée"]
        répétitions = 1
        rebonds = 8
#

Ajustements en fonction de patern_en_cours

        if variable["répétition"]:
            if patern_en_cours == 1:
                répétitions = 8
                rebonds = 0
            if patern_en_cours == 3:
                répétitions = 8
                rebonds = 8

        else:
            répétitions = 1
            rebonds = 8
#

Calcul du diamètre de base en fonction du type

        if variable["type"] == "Sixteenth":
            initial_diameter = notes / durée * 3
        elif variable["type"] == "Long_staccato":
            initial_diameter = notes / durée * 50
        elif variable["type"] == "Long_held":
            initial_diameter = notes * 20
        else:
            initial_diameter = notes
#

Préfix selon patern_en_cours

        if patern_en_cours == 1:
            prefix = "precedent_"
        elif patern_en_cours == 2:
            prefix = "cible_"
        elif patern_en_cours == 3:
            prefix = "suivant_"
#

On génère la direction aléatoire 1 seule fois pour chaque pattern

        origin = mathutils.Vector((0, 0, 0))
        direction = random_unit_vector()

        for _ in range(répétitions):
#

Ajuster rebonds

            if patern_en_cours == 3:
                rebonds -= 1
            if patern_en_cours == 1:
                rebonds += 1

            for rebond_index in range(rebonds):
#

Calcul intensité courante

                if patern_en_cours == 1:
                    intensity_current = rebonds - rebond_index
                else:
                    intensity_current = max(8 - rebond_index, 1)
#

Intensité suivante pour “rebond_index + 1”

                if rebond_index < rebonds - 1:
                    if patern_en_cours == 1:
                        intensity_next = rebonds - (rebond_index + 1)
                    else:
                        intensity_next = max(8 - (rebond_index + 1), 1)
                else:
#

Dernier rebond => intensité identique ou au choix

                    intensity_next = intensity_current
#

Convertir intensités en diamètres

                diameter_current = (
                    fraction_for_intensity(intensity_current) * initial_diameter
                )
                diameter_next = (
                    fraction_for_intensity(intensity_next) * initial_diameter
                )
#

Trouver point d’impact

                end_point = find_intersection(origin, direction, half_size)
                length = (end_point - origin).length
                location = (origin + end_point) / 2
#

(A) CRÉER LE TRONC DE CÔNE

                bpy.ops.mesh.primitive_cone_add(
                    vertices=32,
                    radius1=diameter_current / 2,
                    radius2=diameter_next / 2,
                    depth=length,
                    location=location,
                )
                cone = bpy.context.object
                cone.name = f"{prefix}{cone.name}_rebond_{rebond_index}"
#

Aligner le tronc de cône

                cone_vector = end_point - origin
                cone.rotation_mode = "QUATERNION"
                cone.rotation_quaternion = cone_vector.to_track_quat("Z", "Y")
#

=== (B) CRÉER UNE SPHÈRE À LA JONCTION (end_point) === On lui donne le diamètre du “sommet” du tronçon (diameter_next).

                sphere_radius = diameter_next / 2
                bpy.ops.mesh.primitive_uv_sphere_add(
                    radius=sphere_radius, location=end_point
                )
                sphere = bpy.context.object
                sphere.name = f"{prefix}{sphere.name}_rebond_{rebond_index}"
#

(Optionnel) On peut lui donner le même matériau que le cône

                if cone.data.materials:
                    sphere.data.materials.append(cone.data.materials[0])
#

(C) Calculer la direction de rebond

                normal = mathutils.Vector((0, 0, 0))
                eps = 1e-4
                if abs(end_point.x - half_size) < eps:
                    normal = mathutils.Vector((1, 0, 0))
                elif abs(end_point.x + half_size) < eps:
                    normal = mathutils.Vector((-1, 0, 0))
                elif abs(end_point.y - half_size) < eps:
                    normal = mathutils.Vector((0, 1, 0))
                elif abs(end_point.y + half_size) < eps:
                    normal = mathutils.Vector((0, -1, 0))
                elif abs(end_point.z - half_size) < eps:
                    normal = mathutils.Vector((0, 0, 1))
                elif abs(end_point.z + half_size) < eps:
                    normal = mathutils.Vector((0, 0, -1))

                direction = reflect_vector(direction, normal).normalized()
                origin = end_point

if rep == 1:
    for variable in bibliothèque_filtrée:

        patern_en_cours += 1

        notes = variable["notes"]
        durée = variable["durée"]
        répétitions = 16
        rebonds = 8
#

Ajustements en fonction de patern_en_cours

        if variable["répétition"]:
            if patern_en_cours == 1:
                répétitions = 8
                rebonds = 0
            if patern_en_cours == 3:
                répétitions = 8
                rebonds = 8

        else:
            répétitions = 1
            rebonds = 8
#

Calcul du diamètre de base en fonction du type

        if variable["type"] == "Sixteenth":
            initial_diameter = notes / durée * 2
        elif variable["type"] == "Long_staccato":
            initial_diameter = notes / durée * 100
        elif variable["type"] == "Long_held":
            initial_diameter = notes * 10
        else:
            initial_diameter = notes
#

Préfix selon patern_en_cours

        if patern_en_cours == 1:
            prefix = "precedent_"
        elif patern_en_cours == 2:
            prefix = "cible_"
        elif patern_en_cours == 3:
            prefix = "suivant_"
#

On génère la direction aléatoire 1 seule fois pour chaque pattern

        origin = mathutils.Vector((0, 0, 0))
        direction = random_unit_vector()

        for _ in range(répétitions):
#

Ajuster rebonds

            if patern_en_cours == 3:
                rebonds -= 1
            if patern_en_cours == 1:
                rebonds += 1

            for rebond_index in range(rebonds):
#

Calcul intensité courante

                if patern_en_cours == 1:
                    intensity_current = rebonds - rebond_index
                else:
                    intensity_current = max(8 - rebond_index, 1)
#

Intensité suivante pour “rebond_index + 1”

                if rebond_index < rebonds - 1:
                    if patern_en_cours == 1:
                        intensity_next = rebonds - (rebond_index + 1)
                    else:
                        intensity_next = max(8 - (rebond_index + 1), 1)
                else:
#

Dernier rebond => intensité identique ou au choix

                    intensity_next = intensity_current
#

Convertir intensités en diamètres

                diameter_current = (
                    fraction_for_intensity(intensity_current) * initial_diameter
                )
                diameter_next = (
                    fraction_for_intensity(intensity_next) * initial_diameter
                )
#

Trouver point d’impact

                end_point = find_intersection(origin, direction, half_size)
                length = (end_point - origin).length
                location = (origin + end_point) / 2
#

(A) CRÉER LE TRONC DE CÔNE

                bpy.ops.mesh.primitive_cone_add(
                    vertices=32,
                    radius1=diameter_current / 2,
                    radius2=diameter_next / 2,
                    depth=length,
                    location=location,
                )
                cone = bpy.context.object
                cone.name = f"{prefix}{cone.name}_rebond_{rebond_index}"
#

Aligner le tronc de cône

                cone_vector = end_point - origin
                cone.rotation_mode = "QUATERNION"
                cone.rotation_quaternion = cone_vector.to_track_quat("Z", "Y")
#

=== (B) CRÉER UNE SPHÈRE À LA JONCTION (end_point) === On lui donne le diamètre du “sommet” du tronçon (diameter_next).

                sphere_radius = diameter_next / 2
                bpy.ops.mesh.primitive_uv_sphere_add(
                    radius=sphere_radius, location=end_point
                )
                sphere = bpy.context.object
                sphere.name = f"{prefix}{sphere.name}_rebond_{rebond_index}"
#

(Optionnel) On peut lui donner le même matériau que le cône

                if cone.data.materials:
                    sphere.data.materials.append(cone.data.materials[0])
#

(C) Calculer la direction de rebond

                normal = mathutils.Vector((0, 0, 0))
                eps = 1e-4
                if abs(end_point.x - half_size) < eps:
                    normal = mathutils.Vector((1, 0, 0))
                elif abs(end_point.x + half_size) < eps:
                    normal = mathutils.Vector((-1, 0, 0))
                elif abs(end_point.y - half_size) < eps:
                    normal = mathutils.Vector((0, 1, 0))
                elif abs(end_point.y + half_size) < eps:
                    normal = mathutils.Vector((0, -1, 0))
                elif abs(end_point.z - half_size) < eps:
                    normal = mathutils.Vector((0, 0, 1))
                elif abs(end_point.z + half_size) < eps:
                    normal = mathutils.Vector((0, 0, -1))

                direction = reflect_vector(direction, normal).normalized()
                origin = end_point
#

8) Assignation des matériaux aux objets selon leur nom (couleurs)

Crée et assigne un matériau avec une couleur donnée.

def assign_material(obj, color):
#
    if obj.type == "MESH":
        mat = bpy.data.materials.new(name=f"Material_{color}")
        mat.use_nodes = True
        nodes = mat.node_tree.nodes
        links = mat.node_tree.links
#

Effacer les noeuds existants

        for node in nodes:
            nodes.remove(node)
#

Principled BSDF + Material Output

        principled_node = nodes.new(type="ShaderNodeBsdfPrincipled")
        output_node = nodes.new(type="ShaderNodeOutputMaterial")
        links.new(principled_node.outputs["BSDF"], output_node.inputs["Surface"])
#

Couleur de base

        principled_node.inputs["Base Color"].default_value = (*color, 1)
#

Assigner le matériau à l’objet

        if len(obj.data.materials) == 0:
            obj.data.materials.append(mat)
        else:
            obj.data.materials[0] = mat


color_mapping = {
    "cible": (0.8, 0.40, 0.40),  # Vert
    "precedent": (0.8, 0.60, 0.45),  # Bleu
    "suivant": (0.95, 0.60, 0.30),  # Rouge
    "Cube": (0, 0, 0),  # Noir
}
#

Appliquer les couleurs aux objets correspondants

bpy.ops.object.select_all(action="DESELECT")

for category, color in color_mapping.items():
    bpy.ops.object.select_all(action="DESELECT")
    for obj in bpy.data.objects:
        if obj.name.startswith(category):
            obj.select_set(True)
    for obj in bpy.context.selected_objects:
        assign_material(obj, color)